
from typing import Iterable, List

from .CodeFormatter import CodeFormatter

from .MethodCall import AndOperatorMethodCall, ArgActor, ArgVar, EqualOperatorMethodCall, MethodCall, GetItemMethodCall, OrOperatorMethodCall
from .Commander import Commander, CommanderManager, Statement
import pandas as pd
import uuid


class Proxy(object):
    def __init__(self) -> None:
        self.cmdManager = CommanderManager()

    def with_cmd(self, name: str = '') -> Commander:
        cmd = Commander(name)
        self.cmdManager.append(cmd)
        return cmd

    def __getattr__(self, name):
        st = self.cmdManager.get_last_cmd().get_last_statement()
        call = MethodCall(name)
        st.addCall(call)
        return st

    def __getitem__(self, key):
        st = self.cmdManager.get_last_cmd().get_last_statement()
        call = GetItemMethodCall()
        st.addCall(call)
        call.set_args(key)
        return st

    def __or__(self, other):
        st = self.cmdManager.get_last_cmd().get_last_statement()
        call = OrOperatorMethodCall()
        st.addCall(call)
        call.set_args(other)
        return st

    def __and__(self, other):
        st = self.cmdManager.get_last_cmd().get_last_statement()
        call = AndOperatorMethodCall()
        st.addCall(call)
        call.set_args(other)
        return st

    def __eq__(self, other):
        st = self.cmdManager.get_last_cmd().get_last_statement()
        call = EqualOperatorMethodCall()
        st.addCall(call)
        call.set_args(other)
        return st

    def to_code(self):

        calls = (
            f'{c.to_code()}'
            for c in self.cmdManager._cmds
            if not c.is_empty()
        )

        code = '\n'.join(calls)
        return code


class CallerProxy(Proxy):

    def __init__(self) -> None:
        super().__init__()

    def set_obj_to_var(self, var_name: str, obj):
        self.cmdManager.addVar(var_name, obj)

    # def __getattr__(self, name):
    #     call = super().__getattr__(name)
    #     call.on_after_call(lambda _: self)
    #     return call

    def run(self, res_var_name: str = None):

        obj = None
        var_dict = self.cmdManager.var_dict

        for cmd in self.cmdManager.get_cmds():

            for s in cmd.statements:
                obj = s.run(var_dict)

                if s.ret_name:
                    self.cmdManager.addVar(s.ret_name, obj)

        if res_var_name:
            return self.cmdManager.getVar(res_var_name)

        return obj if (obj is not None) else self.cmdManager.get_last_used_var_ret()


class PandasProxy(Proxy):

    def __init__(self, pandas_name) -> None:
        super().__init__()
        self.pandas = pandas_name

    def get_df(self):
        read_call = self.cmdManager._cmds[-1].calls[0]
        res = None

        try:
            # res = getattr(self.pandas, read_call.name)(
            #     *read_call.args, **read_call.kwargs)
            res = read_call.run(self.pandas)
        except Exception as ex:
            read_call.set_exception(ex)
            raise

        return res


class DataFrameProxy(Proxy):

    def __init__(self, df: pd.DataFrame) -> None:
        Proxy.__init__(self)
        self.df = df

    def __getattr__(self, name):
        print('__getattr__', name)
        call = super().__getattr__(name)
        call.on_after_call(lambda _: self)
        return call

    def get_df(self):
        pass
        res = self.df.copy()
        for cmd in self.cmdManager._cmds:

            for c in cmd.calls:
                try:
                    # res = getattr(res, c.name)(*c.args, **c.kwargs)
                    res = c.run(res)
                except Exception as ex:
                    c.set_exception(ex)
                    raise

        return res

    # def pop_last_call(self) -> MethodCall:
    #     return self.calls.pop()

    # def remove_all_exception_call(self):
    #     self.calls = [c for c in self.calls if not c.has_ex]

    def __str__(self) -> str:
        return str(self.get_df())

    def to_code(self):
        act_cmds = [
            c
            for c in self.cmdManager._cmds
            if not c.is_empty()
        ]

        # df 确保所有命令都有 ret，如果没有指定，则与 caller 一样
        def _reset_ret(cmd: Commander):
            if not cmd.ret_name:
                cmd.ret_name = cmd.caller_name

        # 确保 本次命令调用变量为上次的结果变量
        def _set_start_pre_ret(cmd: Commander, pre_cmd: Commander):
            if pre_cmd and cmd.caller_name != pre_cmd.ret_name:
                cmd.caller_name = pre_cmd.ret_name

        pre_cmd = None
        for c in act_cmds:
            _reset_ret(c)
            _set_start_pre_ret(c, pre_cmd)
            pre_cmd = c

        calls = (
            f'{c.to_code()}\n'
            for c in act_cmds
            if not c.is_empty()
        )

        code = ''.join(calls)
        return code


class ProxyManager(object):

    s_read_func_mapping = {
        'xlsx': 'read_excel',
        'xls': 'read_excel',
        'xlsm': 'read_excel',
        'csv': 'read_csv',
        'feather': 'read_feather',
    }

    def __init__(self) -> None:
        self.df_proxy = CallerProxy()
        self.pd_read_proxy = CallerProxy()
        self.source_cache: pd.DataFrame = None

    def _get_all_proxys(self) -> Iterable[CallerProxy]:
        yield self.pd_read_proxy
        yield self.df_proxy

    def read_data(self, file, filename, file_ext, read_kws):

        # 目前先这样子处理，每次加载数据，重新开一个新的 caller
        self.df_proxy.cmdManager.clear_all()
        self.pd_read_proxy.cmdManager.clear_all()

        self.pd_read_proxy.set_obj_to_var('pd', pd)

        pd_px = self.pd_read_proxy

        with pd_px.with_cmd('加载数据') as cmd:
            cmd.create_statement('pd', 'df')
            func_name = ProxyManager.s_read_func_mapping[file_ext]
            getattr(pd_px, func_name)(ArgActor(file, filename), **read_kws)

        df = pd_px.run('df')
        self.source_cache = df

    def get_df_data(self) -> pd.DataFrame:
        self.df_proxy.set_obj_to_var('df', self.source_cache)
        ret = self.df_proxy.run()
        self.df_proxy.cmdManager.clear_all_var()
        return ret

    def query(self, query_str: str):
        df_px = self.df_proxy

        with df_px.with_cmd('筛选') as cmd:
            cmd.create_statement('df', 'df')
            df_px.query(query_str)

    def groupby(self, keys, aggs):
        df_px = self.df_proxy

        with df_px.with_cmd('分组统计') as cmd:
            cmd.create_statement('df', 'df')
            df_px.groupby(keys).agg(aggs).reset_index()

    def filter(self, filters):
        """
        filters : [(字段名,值list)]
        [('name',[1,2,3]),('age',[10,20,30])]
        """

        def _get_code(name, values):
            if len(values) > 1:
                return f'({name} in {values})'

            return f'({name} == {CodeFormatter.type2str(values[0])})'

        df_px = self.df_proxy
        cmd_name = '筛选(表格操作)'
        last_cmd = df_px.cmdManager.get_last_cmd()

        if (last_cmd is not None) and last_cmd.name == cmd_name:
            df_px.cmdManager.remove_cmd(last_cmd._id)

        filters = list(_get_code(name, values) for name, values in filters)
        cond = ' and '.join(filters)

        if len(filters) == 0:
            return

        with df_px.with_cmd(cmd_name) as cmd:
            cmd.create_statement('df', 'cond')
            df_px.eval(cond)

            cmd.create_statement('df', 'df')
            df_px[ArgVar('cond')]

    def _get_all_cmds(self) -> Iterable[Commander]:
        return (
            m
            for p in self._get_all_proxys()
            for m in p.cmdManager.get_cmds()
        )

    def get_cmds(self) -> List[Commander]:
        cmds = list(self._get_all_cmds())
        return cmds

    def remove_cmd(self, cmd_id: str):
        for p in self._get_all_proxys():
            p.cmdManager.remove_cmd(uuid.UUID(cmd_id))

    def to_code(self) -> str:

        codes = (
            p.to_code()
            for p in self._get_all_proxys()
        )

        ret = '\n'.join(codes)
        return ret
